package evmodel;

// Optimization Packages from Apache Math
import org.apache.commons.math.optimization.linear.*;
import org.apache.commons.math.optimization.RealPointValuePair;
import org.apache.commons.math.optimization.GoalType;

import java.util.Collection;
import java.util.ArrayList;
import java.io.Serializable;

/**
 * Die Klasse event_model beschreibt einen einzelnen Ladevorgang nach Dissertation
 * von Michael Agsten an der TU Ilmenau 10/2005 - 10/2011 abgeleitet aus dem Mini-E
 * 1.0 Projekt 07/2009 - 07/2010.
 *
 * Ein einzelner Ladevorgang wird immmer beschrieben durch seine technischen und
 * zeitlichen Parameter. Diese sind: plug-in, plug_off, start_charge, end_charge,
 * power und consumption. In dem vorliegenden Modell ist ein einzelner Ldevorgang
 * nach dem vereinfachten CCCV Prinzip (siehe Dissertation M. Agsten) beschrieben.
 * Das bedeutet - jeder Ladevorgang kann durch eine Rechteckfunktion beschrieben
 * werden. Das zugrunde liegende Optimierungsmodell basiert auf Linearer Programmierung.
 *
 * @author Michael Agsten, (c) 2010, Ilmenau University of Technology, 2010
 * @author <a href="mailto:agsten.michael@googlemail.com">agsten.michael@googlemail.com</a>
 * @version 1.0
 * @since JDK 1.6
 **/
public class ev implements Cloneable, Serializable {

    // Definition der Merkmale eines Ladevorgangs
    
    public String optimizer;
    
    // Zeitdauzer [Plug-In, Plug-Off]
    public  int plugin  = 0;
    public  int plugoff = 0;
    
    // Diskretisierungsintervall
    public  double T    = 1 / 4;
    
    // Nicht geladene Energiemengen
    public  double not_supplied = 0;
    
    // Objective Werte -> Zielfunktionswerte
    public  double managed_objective_value  = 0;
    public  double unmanged_objective_value = 0;
    
    // Bedarf der Batterie und der Heizung
    private double ebattery = 0;
    private double eheat    = 0;
    
    // Ladeleistung, Wirkungsgrad und cosphi
    private double apower = 0;
    private double mu     = 0;
    public  double cosphi = 0;
    
    // true: gesteuerter Ladevorgang, false: ungesteuerter Ladevorgang
    public boolean managed = false;

    // Arrays
    public double[] objective;
    public double[] llm;
    private double[] shape;
    private double[] unmanaged_shape;


    /**
     * Konstruktur der Klasse ev
     *
     * Die Plug-In Zeit (plugin) wird Java intern um einen Tastpunkt vorgeschoben, so dass ein Plug-In von 1 0 entspricht. Matlab/Simulink verwendet Arrays, die
     * beginnend mit 1 indiziert werden, Java hingegen indiziert beginnend mit Null. Jeder Ladevorgang wird auf 96 Tastpunkte limitiert. Dies berücksichtigt auch
     * die möglichen Vorhersagehorizonte einer Optimierung mit Smart Charge Protokollen.
     *
     * @param plugin        : Plug-In Zeit
     * @param plugoff       : Plug-Off Zeit
     * @param ebattery      : Elektrischer Ladebedarf der Batterie in kWh
     * @param eheat         : Elektrischer Heizbedarf des Fahrzeugs in kWh
     * @param mu            : Systemwirkungsgrad AC/DC Wandlers
     * @param cosphi        : cosphi des AC/DC Wandlers
     * @param apower        : Scheinleistung des AC/DC Wandlers
     *
     *
     */
    public ev(int plugin, int plugoff, double ebattery, double eheat, double mu, double cosphi, double apower) {
        
        // Da die Java interne Indizierung von Array verwendet wird, müssen die Indizes mit um -1 verschoben werden 
        this.plugin  = plugin - 1;
        this.plugoff = plugoff - 1;

        // Es werden nur Ladevorgänge unterstützt, die nicht länger als 96 Tastschritte lang sind
        if ((this.plugoff - this.plugin) > 95) {
            this.plugoff = this.plugin + 95;
            System.out.println("Plug-In und Plug-Off verweisen auf einen Ladevorgang mit mehr als ");   
        }

        // Setzen der internen Variablen
        this.ebattery   = ebattery;     // elektrischer Ladebedarf der Batterie
        this.eheat      = eheat;           // elektrischer Heizbedarf
        this.apower     = apower;         // Scheinleistung
        this.mu         = mu;                 // Wirkungsgrad
        this.cosphi     = cosphi;         // cosphi
        this.T          = 0.25;                // (1/4 Stunde)

        // Fülle das Ladeprofil der ungesteuerten Ladung
        if (this.eheat != 0) {
            fill_shape_unmanaged(apower);
        } else {
            // fast mode for initializiation == non controlled charge
            double effective_energy_needs = this.ebattery / this.mu / this.cosphi;
            double job = 0;
            int ii = 0;

            this.shape = new double[this.plugoff - this.plugin + 1];

            while (((job + this.apower/4*this.mu*this.cosphi) < effective_energy_needs) && (ii < this.shape.length)) {
                this.shape[ii] = this.apower;
                ii++;
                job = job + this.apower/4*this.mu*this.cosphi;
            }

            if ((job < effective_energy_needs) && ii < this.shape.length)
            {
                double diff = effective_energy_needs - job;
                double mpower = diff*4/(this.mu*this.cosphi);
                this.shape[ii] = Math.min(this.apower, mpower);
            }
            this.unmanaged_shape = this.shape.clone();
        }

       // initalsetzen
       this.optimizer = "not optimized";
        
       this.managed = false;
    }

    /**
     * Konstruktur, der eine Kopie eines Ladevorgangs erzeugt.
     *
     * @param event
     */
    public ev(ev event) {
        this.plugin = event.plugin;
        this.plugoff = event.plugoff;
        this.ebattery = event.ebattery;
        this.eheat = event.eheat;
        this.apower = event.apower;
        this.mu = event.mu;
        this.cosphi = event.cosphi;
        this.T = event.T;
        this.objective = event.objective;
        this.not_supplied = event.not_supplied;
        this.unmanaged_shape = event.unmanaged_shape;
        this.shape = event.shape;

    }

    /**
     * Ungesteuertes Laden (Berechnung des Leistungsprofils) mit Limitierung der Ladeleistung
     * nach SAEJ1772 & IEC62196
     *
     * @param cp_limit      : Zeitkonstante maximale Scheinleistung im Austausch zwischen Fahrzeug und Netz.
     */
    public final void fill_shape_unmanaged(double cp_limit) {

        // Erzeuge eine Dummy Zielfunktion, die aufsteigend zur Zeit anwächst
        double dummy[] = new double[this.plugoff - this.plugin + 1];
        for (int ii = 0; ii < dummy.length; ii++) {
            dummy[ii] = dummy.length - ii;
        }
        this.solve_managed_charge(dummy, cp_limit);

        // Aufruf der Optimierungsprozedur
        this.unmanaged_shape = this.shape.clone();
    }

    /**
     * Optimierung des Ladeprofils mit einer konstanten Ladescheinleistung
     *
     * @param objective     : Zielfunktion
     * @param cp_limit      : Zeitkonstante maximale Scheinleistung im Austausch zwischen Fahrzeug und Netz.
     */
    public void solve_managed_charge(double[] objective, double cp_limit) {

        // LLM Funktion (Lokales Lastmanagement) - muss die gleiche Länge haben wie die Zeilfunktion
        double[] dummy = new double[objective.length];

        // Füllt die LLM Funktion mit dem konstanten Limit am Ladepunkt auf
        for (int ii = 0; ii < objective.length; ii++) {
            dummy[ii] = cp_limit;
        }

        // Aufruf der Optimierungsprozedur
        this.solve_managed_charge(objective, dummy);
    }

    /**
     * Optimierung des Ladeprofils mit einer zeitvariablen Ladescheinleistung
     *
     * @param objective     : Zielfunktion
     * @param cp_limit      : Zeitvariable maximale Scheinleistung im Austausch zwischen Fahrzeug und Netz.
     */
    public void solve_managed_charge(double[] objective, double[] cp_limit) {

        //Prüfung, ob Ladevorgang optimierbar ist
        if (objective.length != cp_limit.length) {
            System.out.println("Optimierung fehlgeschlagen. Die Übergabeelemente sind unterschiedlich lang.");
        }

        // Speichern der gegebenen Zielfunktionen LLM und Objective
        this.objective = objective;
        this.llm = cp_limit;

        // Berechnung der potentiell ladbaren Energie am Ladepunkt im gegebenen Zeitintervall
        double potential_energy = 0;

        // Über alle Tastpunkte
        for (int ii = 0; ii < cp_limit.length; ii++) {
            // Die LLM Funktion muss noch geprüft werden hinsichtlich Erfüllbarkeit durch das Fahrzeug
            cp_limit[ii] = Math.min(cp_limit[ii], this.apower);

            // Berechnung der potentiellen Energieaufnahme je Tastpunkt und Saldierung in einem Skalar
            potential_energy = potential_energy + cp_limit[ii] * this.T;
        }

        // Es wird verglichen, ob die potentiell mögliche ladbare Leistung am Ladepunkt den Energiebedarf limitiert
        if (((this.ebattery + this.eheat) / this.mu / this.cosphi) >= potential_energy) {

            // es ist weniger Energie ladbar als gefordert wird
            this.shape = cp_limit;

            // Berechnung nicht geladener Energie
            this.calc_notsupplied();

        } else {
            
            if (this.eheat==0)
            {
                this.solve_qs();
                this.optimizer="Quick Sort";
            }
            else
            {
                         
            /*
             * Anwendung des Optimierungsverfahrens
             *
             * max(c'*p| A*p <= b)
             *
             * b - Randbedingung
             * A - gefüllt mit Zeilenvektoren
             * c - Zielfunktione
             * p - Ladeleistung
             *
             */


            // Randbededingungen (Typ der Apache Mathlibrary)
            Collection constraints = new ArrayList();

            // Prüfen, wie viele Schritte die Beheizung benötigt
            double job = 0;
            int c_step = cp_limit.length - 1;

            // Es wird davon ausgegangen, dass zum Ende der Anschlusszeit geheizt wird --> Ermittlung der benötigten Zeitschritte
            while (job < this.eheat / this.mu / this.cosphi) {
                job = job + cp_limit[c_step] * this.T;
                c_step--;
            }

            /* Füllung der Randbedingung für die Heizung
             *
             * A(:,1)* >= b(1)
             *
             * Die Summe der Leistungen zu den mit 1 gefüllten Taspunkt soll größer gleich der angegebenen Menge zur Beheizung der Fahrzeuge (b(1)=E_heat) sein.
             * Die Darstellung folgt hier Matlab in der <= Präsentation: Multiplikation der gesamten Gleichung mit -1
             *
             * -A(1,:)*p < -b(1)
             *
             * Wird nur hinzugeführt, wenn der Heizbedarf größer Null ist.
             */

            if (this.eheat > 0) {
                // Constraint für Heizung
                double[] climate = new double[cp_limit.length];

                // Füllung mit "1" solange ii größer der angegebenen Anzahl an Schritten zur Beheizung ist.
                for (int ii = 0; ii < climate.length; ii++) {
                    if (ii > c_step) {
                        climate[ii] = -1;
                    }
                }

                // Hinzufügen der Randbedingung nach Apache MathLibrary
                constraints.add(new LinearConstraint(climate, Relationship.LEQ, -this.eheat / this.T / this.cosphi / this.mu));
            }

            /* Füllung der Randbedingung, dass durch den Maximalansatz die Gesamtenergieaufnahme nicht überschritten wird
             * 
             * A(2,:)*p<=b(2)
             * 
             * Die Summe der Leistungen in p darf im Ladehorizont nicht die angegebene maximale Energieaufnahme überschreiten.
             * 
             */

            // Füllung der Übergabevariable charge = A(2,:) mit 1/0 für alle Tastpunkte mit 1
            double[] charge = new double[cp_limit.length];
            for (int ii = 0; ii < charge.length; ii++) {
                charge[ii] = 1;
            }

            // Hinzufügen der Randbedingung nach Apache MathLibrary
            constraints.add(new LinearConstraint(charge, Relationship.LEQ, (this.eheat + this.ebattery) / this.T / this.cosphi / this.mu));

            /* Füllung der Randbedingung für die Ladeleistung zum k-ten Zeitpunkt unter Berücksichtigung der LLM Vorgaben
             *
             * A(k,:)*p <= b(k)
             *
             * Zu jedem Tastpunkt ist sicherzustellen, dass die Scheinleistung nicht größer wird als die LLM Vorgabe.
             *
             */

            // für jeden Tastpunkt
            for (int ii = 0; ii < cp_limit.length; ii++) {

                // Zeile A(k,:)
                double[] job2 = new double[cp_limit.length];

                // Jedes Element in A(k,:) -> A(k,jj)
                for (int jj = 0; jj < cp_limit.length; jj++) {

                    // wenn ii==jj, dann ist der Tastpunkt relevant
                    if (ii == jj) {
                        job2[jj] = 1;
                    } else {
                        job2[jj] = 0;
                    }
                }

                // Hinzufügen der Randbedingung nach Apache MathLibrary
                constraints.add(new LinearConstraint(job2, Relationship.LEQ, cp_limit[ii]));
                constraints.add(new LinearConstraint(job2, Relationship.GEQ, 0));
            }

            // Hinzufügen der Objective Funktion nach Apache MathLibrary
            LinearObjectiveFunction f = new LinearObjectiveFunction(objective, 0);

            // Lösen des Optimierungsproblems
            solve_lp(f, constraints);
            this.optimizer="Linear Programming";
            }
            
            this.managed = true;

        }
    }

    /**
     * Schnelle Lösung des Ladevorgangs
     * 
     * 
     */
    private void solve_qs()
    {
        
        // verwende den simplen optimierer
        int[] index = new int[this.objective.length];
        double[] profile = new double[this.objective.length];

        for (int ii = 0; ii < index.length; ii++) {
            index[ii] = ii;
        }

        quicksort(objective.clone(), index);

        double charged = 0;

        int idx = 0;

        while (((charged + this.llm[idx] / 4) < this.ebattery) && (idx < profile.length)) {
            profile[index[idx]] = this.llm[idx];
            charged += apower / 4;
            idx++;
        }

        if ((charged < this.ebattery) && (idx < profile.length)) {
            profile[index[idx]] = (this.ebattery - charged);
        }
    
        this.shape=profile;
    }
    
    
    /**
     * Methode, die den Optimierer der Apache MathLibrary aufruft
     *
     * @param f             : LinearObjectiveFunction / Apache MathLibrary
     * @param constraints   : Collection / Apache MathLibrary
     */
    private void solve_lp(LinearObjectiveFunction f, Collection constraints) {

        // eingekapselt in eine try and catch Prozedur --> es kann sein, dass der Optimierer keine Lösung findet
        try {
            // Instanz des Simplexsolvers
            SimplexSolver solver = new SimplexSolver();

            // Modifikation der Anzahl der Lösungsschritte
            solver.setMaxIterations(2 * 96);

            // Aufruf des Optimierers
            RealPointValuePair solution = solver.optimize(f, constraints, GoalType.MAXIMIZE, false);

            // Schreiben der Ergebnisvariablen
            this.shape = solution.getPoint();
        } catch (Exception e) {
            // im Fehlerfall erfolgt eine Ausgabe
            System.out.println("Problem ist nicht lösbar " + e);
            System.out.println("Plug-In : " + this.plugin);
            System.out.println("Plug-Off: " + this.plugoff);
            System.out.println("EBattery: " + this.ebattery);
            System.out.println("EHeat   : " + this.eheat);
            System.out.println("Mu      : " + this.mu);
            System.out.println("CosPhi  : " + this.cosphi);
        }
    }

    /**
     * Berechnet in kWh die nicht versorgte Menge
     *
     */
    private void calc_notsupplied() {
        double job = 0;
        for (int ii = 0; ii < this.shape.length; ii++) {
            job = job + this.shape[ii] * this.T * this.mu * this.cosphi;
        }
        this.not_supplied = this.ebattery + this.eheat;
    }

    /**
     * Gibt das optimierte Leistungsprofil zurück
     *
     * @return Leistungsprofil als double[]
     */
    public double[] get_shape() {
        return this.shape;
    }

    /**
     * Schreibt manuell das Leistungsprofil, dies kann der Fall sein, wenn andere Optimierer zum Einsatz gekommen sind
     *
     * @param shape
     */
    public void set_shape(double[] shape) {
        if (this.shape.length != shape.length) {
            System.out.println("Shapelänge passt nicht zu den Angaben der Ladung. Array muss " + this.shape.length + " Elemente lang sein.");
        } else {
            this.shape = shape;
        }

    }

    /**
     * Methode mit der eine Verschiebung des Ladevorganges um eine Woche in die Vergangenheit
     * geschoben werden kann. Der Verschiebefaktor ist 7*96 1/4h Werte groß. Diese Funktion
     * hat nichts mit den allgemeinen Ladeverschiebungen zu tun. Sie ändert die Randbedingungen
     * in denen der Ladevorgang stattfindet allgemein. Für spezifische Verschiebungen innerhalb
     * des Ladeintervalls ist die Funktion shift_charge zuständig.
     */
    public void shift_to_one_week_before() {
        int shifttime = 7 * 96;
        this.plugin = this.plugin - shifttime;
        this.plugoff = this.plugoff - shifttime;
    }

    /**
     * Methode mit der eine Verschiebung des Ladevorganges um eine Woche in die Vergangenheit
     * geschoben werden kann. Der Verschiebefaktor ist 7*96 1/4h Werte groß. Diese Funktion
     * hat nichts mit den allgemeinen Ladeverschiebungen zu tun. Sie ändert die Randbedingungen
     * in denen der Ladevorgang stattfindet allgemein. Für spezifische Verschiebungen innerhalb
     * des Ladeintervalls ist die Funktion shift_charge zuständig.
     */
    public void shift_to_one_week_after() {
        int shifttime = 7 * 96;
        this.plugin = this.plugin + shifttime;
        this.plugoff = this.plugoff + shifttime;
    }

    /**
     * Methode mit der bestimmt wird, ob die Beladung des Fahrzeuges innerhalb des Ladevorganges zum Zeitpunkt time aktiv ist. Gibt 1 oder 0 zurück.
     * Relevant für Optimierungen, die auf Basis von Planungsprozessen agieren.
     *
     * @param time Zeitpunkt für den interessiert, ob geladen wird.
     * @return 1 oder 0 (Integer)
     */
    public int is_charging(int time) {
        if (this.plugoff <= time) {
            return 0;
        } else {
            if (this.charge(time) != 0) {
                return 1;
            } else {
                return 0;
            }
        }
    }

    /**
     * Methode mit der bestimmt wird, ob die ein Fahrzeuges zum Zeitpunkt time verfügbar ist. Gibt 1 oder 0 zurück.
     * Relevant für: Funktionen zur Bestimmung der Verfügbarkeit von Gruppen.
     *
     * @param time Zeitpunkt für den interessiert, ob ein Fahrzeug verfügbar ist.
     * @return 1 oder 0 (Integer)
     */
    public int is_available(int time) {
        // returns wheather a charging process is active or not as int
        if (this.available(time)) {
            return 1;
        } else {
            return 0;
        }
    }

    /**
     * Methode mit der bestimmt wird, wie hoch die Ladeleistung zum Zeitpunkt time ist. Gibt 0 oder die Ladeleistung P(time) zurück.
     * Relevant für die Bestimmung des Ladeprofils.
     * @param time Zeitpunkt für den interessiert, ob geladen wird.
     * @return P(time) oder 0 (Double)
     */

    /*
     *             | ftime            | ttime
    0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
    | offset = 1        | offset = 1

    shape = [2 3 4 5 6 7 8 9 10 11], time = 3
     *
     */
    public double charge(int time) {
        if (this.shape != null) {
            if ((time >= this.plugin) && (time <= this.plugoff)) {
                return this.shape[time - this.plugin];
            } else {
                return 0;
            }
        } else {
            return 0;
        }
    }

    /**
     * Methode mit der bestimmt wird, ob die Beladung des Fahrzeuges innerhalb des Ladevorganges zum Zeitpunkt time aktiv ist. Gibt 1 oder 0 zurück.
     *
     * @param time Zeitpunkt für den interessiert, ob geladen wird.
     * @return 1 oder 0 (Boolean)
     */
    public boolean available(int time) {
        // returns wheather a charging process is active or not as boolean
        if (this.plugin <= time & this.plugoff > time) {
            return true;
        } else {
            return false;
        }
    }

    /**
     * Methode mit der bestimmt wird, ob ein Fahrzeug im Intervall [start stop] verfügbar ist. Gibt 1 oder 0 zurück.
     *
     * @param start (int)
     * @param stop (int)
     * @return 1 oder 0 (Boolean)
     */
    public boolean available(int start, int stop) {
        // returns wheather a charging process is active or not as boolean
        if ((this.plugin >= start) & (this.plugoff < stop)) {
            return true;
        } else {
            return false;
        }
    }

    /**
     * Methode, die das aktuelle Objekt in eine neue Instanz klont. Wird gebraucht, um Objekte in unterschiedlichen Listen verarbeiten zu können.
     *
     * @return Objekt des Typs charge_event
     */
    @Override
    public ev clone() {
        // clones the charging event
        ev job = new ev(this);
        return job;
    }

    /**
     * 
     *
     * @return Gibt die Länge des Ladevorgangs zurück in 1/4h - benötigt, um extern beurteilen zu können wieviele Zeitschritte geladen wird
     */
    public int objective_llm_request_length() {
        return this.plugoff - this.plugin + 1;
    }

    /**
     * Berechnung der erzielten Güte anhand der Objective und LLM Funktion
     *
     */
    public void calc_objective_values() {
        // kalkuliere Vergleichswerte
        this.managed_objective_value = 0;
        this.unmanged_objective_value = 0;

        if (this.objective != null) {
            for (int ii = 0; ii < this.shape.length; ii++) {
                this.unmanged_objective_value += this.objective[ii] * this.unmanaged_shape[ii];
                this.managed_objective_value += this.objective[ii] * this.shape[ii];
            }
        } else {
            this.unmanged_objective_value = 0;
            this.managed_objective_value = 0;
        }
    }

    /**
     * 
     *
     * @return Gibt die Summe des Ladprofils zurück / nicht skaliert auf die Abstastrate
     */
    public double sum_shape() {
        double sum = 0;
        for (int ii = 0; ii < this.shape.length; ii++) {
            sum += this.shape[ii];
        }
        return sum;
    }
    
    
    /**
     * Aufruf des Quicksort Algorithmus
     * 
     * @param main
     * @param index 
     */
    public static void quicksort(double[] main, int[] index) {
        quicksort(main, index, 0, index.length - 1);
    }

    /**
     * Azfruf des Quicksort Algorithmus im Intervall [left, right]
     * 
     * @param a
     * @param index
     * @param left
     * @param right 
     */
    public static void quicksort(double[] a, int[] index, int left, int right) {
        if (right <= left) {
            return;
        }
        int i = partition(a, index, left, right);
        quicksort(a, index, left, i - 1);
        quicksort(a, index, i + 1, right);
    }

    /**
     * Partionierung im Quciksort Algorithmus
     * 
     * @param a
     * @param index
     * @param left
     * @param right
     * @return 
     */
    private static int partition(double[] a, int[] index, int left, int right) {
        int i = left - 1;
        int j = right;
        while (true) {
            while (less(a[++i], a[right])) // find item on left to swap
            ;                               // a[right] acts as sentinel
            while (less(a[right], a[--j])) // find item on right to swap
            {
                if (j == left) {
                    break;           // don't go out-of-bounds
                }
            }
            if (i >= j) {
                break;                  // check if pointers cross
            }
            exch(a, index, i, j);               // swap two elements into place
        }
        exch(a, index, i, right);               // swap with partition element
        return i;
    }

    /**
     * Abfragem ob x kleiner y ist
     * 
     * @param x
     * @param y
     * @return 
     */
    private static boolean less(double x, double y) {
        return (x < y);
    }

    /**
     * Tausch von a und b und setzen des Index
     * 
     * @param a
     * @param index
     * @param i
     * @param j 
     */
    private static void exch(double[] a, int[] index, int i, int j) {
        double swap = a[i];
        a[i] = a[j];
        a[j] = swap;
        int b = index[i];
        index[i] = index[j];
        index[j] = b;
    }
}
